﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Threading;
using Present.Commands.Async.Actions.Decorators;
using Present.Commands.Rx;

namespace Present.Commands.Async.Actions
{



    /// <summary>
    /// A target that can run multiple async targets at once and then do something when they are all finsihed
    /// </summary>
    public class CompositeAsyncAction : AsyncAction<object, object>
    {
        private readonly Action<object> whenAllFinished;
        private readonly Action<Exception> handleError;
        public IObservableCommandAction[] Actions { get; private set; }



        public CompositeAsyncAction(Action<Exception> handleError, params IObservableCommandAction[] actions)
            : this(x => { }, handleError, actions)
        {
        }

        public CompositeAsyncAction(Action<object> whenAllFinished, Action<Exception> handleError, params IObservableCommandAction[] actions)
        {
            this.whenAllFinished = whenAllFinished;
            this.handleError = handleError;
            this.Actions = actions;
            GetAllCanExecuteChangedEvents().Subscribe(x => RaiseCanExecuteChanged());
        }

        private IObservable<EventPattern<EventArgs>> GetAllCanExecuteChangedEvents()
        {
            var events = Actions.Select(action => action.CanExecuteChangedObservable());
            return events.Merge();
        }

        private IDisposable WhenAllFinished(Action<object[]> action)
        {
            return ForkJoin(Actions).ObserveOn(Scheduler.NewThread).Subscribe(action);
        }

        public override void HandleSuccess(object parameter)
        {
            whenAllFinished(parameter);
        }

        public override ActionExecution<object> PrepareWork(object parameter)
        {
            return new CompositeActionExecution(parameter, Actions.Select(x => x.PrepareWork(parameter)).ToArray());
        }

        public override object DoWork(ActionExecution<object> parameter)
        {
            var args = (CompositeActionExecution)parameter;
            var signal = new ManualResetEvent(false);
            using (WhenAllFinished(x => signal.Set()))
            {
                ExecuteActions(args);
                signal.WaitOne();
            }
            return args;
        }

        private void ExecuteActions(CompositeActionExecution execution)
        {
            Debug.Assert(Actions.Length == execution.ActionExecution.Length, "Actions and Args don't match");

            for (int n = 0; n < Actions.Length; n++)
            {
                new CompositeAsyncCommand(Actions[n]).Execute(execution.ActionExecution[n]);
            }
        }

        public override void HandleError(Exception error)
        {
            handleError(error);

        }

        public override bool CanExecute(object parameter)
        {
            return Actions.All(x => x.CanExecute(parameter));
        }


        class CompositeAsyncCommand : AsyncCommand
        {
            public CompositeAsyncCommand(IAsyncAction target)
                : base(target)
            {
            }

            public override void Execute(object parameter)
            {
                RunWorker((ActionExecution)parameter);
            }
        }

        class CompositeActionExecution : ActionExecution<object>
        {
            public ActionExecution[] ActionExecution { get; private set; }

            public CompositeActionExecution(object parameter, params ActionExecution[] execution)
                : base(parameter)
            {
                ActionExecution = execution;
            }
        }


        /// <summary>
        /// FrokJoin is no longer in the latest stable build, moved to experimental
        /// So this is ours until the next release
        /// </summary>
        public static IObservable<TSource[]> ForkJoin<TSource>(params IObservable<TSource>[] sources)
        {
            return sources
                .Select((o, i) => o.TakeLast(1).Select(value => new { i, value }))
                .Merge()
                .Aggregate(
                    new { array = new TSource[sources.Length], count = 0 },
                    (results, result) =>
                    {
                        results.array[result.i] = result.value;
                        return new { results.array, count = results.count + 1 };
                    })
                .Where(results => results.count == sources.Length)
                .Select(results => results.array);
        }

    }
}